跳到主要内容

Java 多线程-线程池 Executors 工具类

Executors 工具类的原理

Java 中的线程池顶层接口是 Executor 接口,ThreadPoolExecutor 是这个接口的实现类。

ThreadPoolExecutor 默认提供的构造方法参数太多了,所以 Executors 又使用静态工厂封装了一层,就是下面的几个静态方法

Executors.newCachedThreadPool():无限线程池。 Executors.newFixedThreadPool(nThreads):创建固定大小的线程池。 Executors.newSingleThreadExecutor():创建单个线程的线程池。

补充个知识点:静态工厂方法与构造器不同的优势在于,它们有名字,由于语言的特性,Java 的构造函数都是跟类名一样的。这导致的一个问题是构造函数的名称不够灵活,经常不能准确地描述返回值,在有多个重载的构造函数时尤甚,如果参数类型、数目又比较相似的话,那更是很容易出错。(实际上还有一个优势,就是稍加改造就可以变成单例模式,但是和这里应用场景不符,所以就不写了)

Executors 工厂类

Executors 类里面提供了一些静态工厂,生成一些常用的线程池,基本就如下这几种

Executors.newCachedThreadPool():无限线程池。 Executors.newFixedThreadPool(nThreads):创建固定大小的线程池。 Executors.newSingleThreadExecutor():创建单个线程的线程池。

它们内部也是通过调用 ThreadPoolExecutor 来创建的,例如这里点进 newCachedThreadPool 看它的源码

public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。此线程池 保证所有任务的执行顺序按照任务的提交顺序执行

public class Temp {

static Integer flag = 0;

public static void main(String[] args) {
ExecutorService ser = Executors.newSingleThreadExecutor();
Runnable runnable = () -> {
System.out.println(++flag);
};

// 可以看到是按顺序打印 flag 1、2、3
ser.execute(runnable);
ser.execute(runnable);
ser.execute(runnable);

// 执行完成并不会暂停服务,需要手动结束服务,这个 shutdown 方法会在所有任务结束之后关闭服务
ser.shutdown();
}
}

如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。

newFixedThreadPool

创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。下面这个例子因为线程池大小是固定的,这里设置的是 3个线程,所以线程名只有3个。

public class Temp {
public static void main(String[] args) {
ExecutorService ser = Executors.newFixedThreadPool(3);
Runnable runnable = () -> {
for (int i = 0; i < 10; i++) {
System.out.println("Hello" + i + Thread.currentThread().getName());
}
};

// 查看打印结果会发现虽然这里开启了四个任务,但只有三个线程在工作
ser.execute(runnable);
ser.execute(runnable);
ser.execute(runnable);
ser.execute(runnable);

ser.shutdown();
}
}

newCachedThreadPool

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。

public class Temp {

public static void main(String[] args) {
ExecutorService ser = Executors.newCachedThreadPool();
Runnable runnable = () -> {
for (int i = 0; i < 10; i++) {
System.out.println("Hello" + i + Thread.currentThread().getName());
}
};
// 这里创建了 5个线程
ser.execute(runnable);
ser.execute(runnable);
ser.execute(runnable);
ser.execute(runnable);
ser.execute(runnable);

ser.shutdown();
}
}

ScheduledThreadPool 定时任务

创建一个定长线程池,支持定时或者周期性任务执行。

public class Temp {
public static void main(String[] args) {
// 注意:这里的接口也要换成 ScheduledExecutorService
ScheduledExecutorService ser = Executors.newScheduledThreadPool(5);
Runnable runnable = () -> {
System.out.println("这是周期任务");
};

// 延时 3秒执行
ser.schedule(runnable, 3, TimeUnit.SECONDS);

// 周期执行任务,这里表示延迟 1 秒后每 3 秒执行一次
ser.scheduleAtFixedRate(runnable, 1, 3, TimeUnit.SECONDS);
}
}

ScheduledExecutorService 比 Timer 更安全,功能更强大

如果任务以固定的每3秒执行,我们可以这样写:

// 2秒后开始执行定时任务,每3秒执行:
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);

如果任务以固定的3秒为间隔执行,我们可以这样写:

// 2秒后开始执行定时任务,以3秒为间隔执行:
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);

Reference

参考资料 线程池的submit和execute的区别 参考资料 Java并发编程:线程池的使用